package com.onluy.frameworkcloud.batis.spring;


import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import name.pachler.nio.file.*;
import name.pachler.nio.file.WatchEvent.Kind;
import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

public class HotSqlSessionFactoryBean extends SqlSessionFactoryBean
{
    private final Logger log;
    private final ResourcePatternResolver resourcePatternResolver;
    private Resource configLocation;
    private XNode mappersNode;
    private boolean isHotDeploy;
    private String sqlLocations;
    private Set<String> hotDeployLocations;

    public HotSqlSessionFactoryBean()
    {
        this.log = LoggerFactory.getLogger(getClass());

        this.resourcePatternResolver = new PathMatchingResourcePatternResolver();
    }

    public void setSqlLocations(String sqlLocations)
            throws IOException
    {
        this.sqlLocations = sqlLocations;

        super.setMapperLocations(findResource());
    }

    public void setHotDeployLocations(Set<String> hotDeployLocations) {
        this.hotDeployLocations = hotDeployLocations;
    }

    protected SqlSessionFactory buildSqlSessionFactory()
            throws IOException
    {
        SqlSessionFactory sqlSessionFactory = super.buildSqlSessionFactory();

        if ((this.isHotDeploy) && (this.hotDeployLocations != null) && (this.hotDeployLocations.size() > 0)) {
            startReloadXML(sqlSessionFactory.getConfiguration());
        }
        return sqlSessionFactory;
    }

    private void startReloadXML(Configuration configuration)
    {
        try
        {
            this.mappersNode = new XPathParser(this.configLocation.getInputStream()).evalNode("/configuration").evalNode("mappers");

            for (String hotDeployLocation : this.hotDeployLocations) {
                String monitorPath = getClass().getClassLoader().getResource("").getPath() + hotDeployLocation;
                new Thread(new Monitor(Paths.get(monitorPath), new AtomicBoolean(false), configuration)).start();
                this.log.debug("Mybatis hot deployment process. Monitor root directory：{}", monitorPath);
            }
        }
        catch (Exception e) {
            this.log.error("Error starting Mybatis hot deployment", e);
        }
    }

    private Resource[] findResource()
            throws IOException
    {
        return this.resourcePatternResolver.getResources(this.sqlLocations);
    }

    public void setIsHotDeploy(boolean isHotDeploy)
    {
        this.isHotDeploy = isHotDeploy;
    }

    public void setConfigLocation(Resource configLocation) {
        this.configLocation = configLocation;
        super.setConfigLocation(configLocation);
    }

    private void mapperElement(Configuration configuration)
            throws Exception
    {
        if (this.mappersNode != null)
            for (XNode child : this.mappersNode.getChildren())
                if ("package".equals(child.getName())) {
                    String mapperPackage = child.getStringAttribute("name");
                    configuration.addMappers(mapperPackage);
                } else {
                    String resource = child.getStringAttribute("resource");
                    String url = child.getStringAttribute("url");
                    String mapperClass = child.getStringAttribute("class");
                    if ((resource != null) && (url == null) && (mapperClass == null)) {
                        ErrorContext.instance().resource(resource);
                        InputStream inputStream = Resources.getResourceAsStream(resource);
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if ((resource == null) && (url != null) && (mapperClass == null)) {
                        ErrorContext.instance().resource(url);
                        InputStream inputStream = Resources.getUrlAsStream(url);
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if ((resource == null) && (url == null) && (mapperClass != null)) {
                        Class mapperInterface = Resources.classForName(mapperClass);
                        configuration.addMapper(mapperInterface);
                    } else {
                        throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                    }
                }
    }

    private class Monitor
            implements Runnable
    {
        private AtomicBoolean stop = null;
        private Configuration configuration = null;

        WatchService watcher = null;
        WatchKey watckKey = null;

        public Monitor(Path path, AtomicBoolean stop, Configuration configuration) {
            this.stop = stop;
            this.configuration = configuration;
            try
            {
                this.watcher = FileSystems.getDefault().newWatchService();

                path.register(this.watcher, new WatchEvent.Kind[] { StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_MODIFY, StandardWatchEventKind.ENTRY_DELETE });
            } catch (Exception e) {
                HotSqlSessionFactoryBean.this.log.error(e.getMessage(), e);
            }
        }

        public void run()
        {
            try
            {
                this.watckKey = this.watcher.take();
            } catch (Exception e) {
                HotSqlSessionFactoryBean.this.log.error(e.getMessage(), e);
            }
            while (!this.stop.get())
                try {
                    List nevents = this.watckKey.pollEvents();
                    if ((nevents != null) && (nevents.size() > 0)) {
                        reloadXML(this.configuration);
                    }
                    Thread.currentThread(); Thread.sleep(500L);
                } catch (Exception ex) {
                    HotSqlSessionFactoryBean.this.log.error(ex.getMessage(), ex);
                }
        }

        public void stop()
        {
            this.stop.compareAndSet(false, true);
        }

        public void reloadXML(Configuration configuration)
                throws Exception
        {
            HotSqlSessionFactoryBean.this.log.debug("File or directory is modified, reload all XML files to start...");
            long beginTime = System.currentTimeMillis();
            removeConfig(configuration);
            Resource[] resources = HotSqlSessionFactoryBean.this.findResource();
            for (Resource resource : resources) {
                try {
                    HotSqlSessionFactoryBean.this.log.debug("加载{}", resource);
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(), configuration, resource.toString(), configuration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } finally {
                    ErrorContext.instance().reset();
                }

            }

            HotSqlSessionFactoryBean.this.mapperElement(configuration);

            double s = (System.currentTimeMillis() - beginTime) / 1000.0D;
            HotSqlSessionFactoryBean.this.log.debug("XML file reload. This reload time consuming {}s. Total load file {}.", Double.valueOf(s), Integer.valueOf(resources.length));
        }

        private void removeConfig(Configuration configuration)
                throws Exception
        {
            Class classConfig = configuration.getClass();
            clearMap(classConfig, configuration, "mappedStatements");
            clearMap(classConfig, configuration, "caches");
            clearMap(classConfig, configuration, "resultMaps");
            clearMap(classConfig, configuration, "parameterMaps");
            clearMap(classConfig, configuration, "keyGenerators");
            clearMap(classConfig, configuration, "sqlFragments");
            clearSet(classConfig, configuration, "loadedResources");
        }

        private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
            Field field = classConfig.getDeclaredField(fieldName);
            field.setAccessible(true);
            ((Map)field.get(configuration)).clear();
        }

        private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
            Field field = classConfig.getDeclaredField(fieldName);
            field.setAccessible(true);
            ((Set)field.get(configuration)).clear();
        }
    }
}